﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Lucene.Net.Documents;
using System.Security.Cryptography;
using Lucandra.Convenience;

namespace Lucandra.Net
{
    internal sealed class LucandraDocument
    {
        #region Fields

        private List<StoredField> storedFields;
        private List<IndexedTerm> indexedTerms;

        #endregion

        #region Properties [public]

        public string IndexName { get; private set; }
        public string DocumentID { get; private set; }
        public IEnumerable<StoredField> StoredFields
        {
            get { return this.storedFields; }
        }
        public int StoredFieldCount
        {
            get { return this.storedFields.Count; }
        }
        public IEnumerable<IndexedTerm> IndexedTerms
        {
            get { return this.indexedTerms; }
        }
        public int IndexedTermCount
        {
            get { return this.indexedTerms.Count; }
        }

        #endregion

        #region Constructors

        public LucandraDocument(string indexName, string documentID)
        {
            this.IndexName = indexName;
            this.DocumentID = documentID;

            this.storedFields = new List<StoredField>();
            this.indexedTerms = new List<IndexedTerm>();
        }

        #endregion

        public void AddStoredField(StoredField storedField)
        {
            this.storedFields.Add(storedField);
        }

        public void AddIndexedTerm(IndexedTerm indexedTerm)
        {
            this.indexedTerms.Add(indexedTerm);
        }

        public Document GetDocument()
        {
            var doc = new Document();

            foreach (var storedField in this.storedFields)
            {
                if (storedField.IsBinary)
                    doc.Add(new Field(storedField.FieldName, storedField.Value, Field.Store.YES));
                else
                    doc.Add(new Field(storedField.FieldName, Encoding.UTF8.GetString(storedField.Value), Field.Store.YES, Field.Index.ANALYZED));
            }

            return doc;
        }

        public string GetDocumentKey()
        {
            //return this.IndexName + Settings.Delimeter + this.DocumentID;
            return LucandraDocument.CreateDocumentKey(this.IndexName, this.DocumentID);
        }

        public static string CreateDocumentKey(string indexName, string documentID)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < indexName.Length; i++)
            {
                if (i % 2 == 0 && i < documentID.Length)
                    sb.Append(documentID[i]);
                else
                    sb.Append(indexName[i]);
            }
            byte[] hash = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(sb.ToString()));
            byte[] newHash = new byte[10];
            Array.Copy(hash, newHash, newHash.Length);
            string hashString = LucandraHelper.CreateHash(newHash);

            return string.Format("{0}{1}{2}{3}", hashString, indexName, Settings.Delimeter, documentID);
        }

        #region Nested Classes

        public class StoredField
        {
            public string FieldName { get; private set; }
            public int ValueIndex { get; private set; }
            public bool IsBinary { get; private set; }
            public byte[] Value { get; private set; }

            public StoredField(string fieldName, int valueIndex, bool isBinary, byte[] value)
            {
                this.FieldName = fieldName;
                this.ValueIndex = valueIndex;
                this.IsBinary = isBinary;
                this.Value = value;
            }

            public string GetStoredFieldKey()
            {
                return this.FieldName + Settings.Delimeter + this.ValueIndex;
            }

            public byte[] GetBytesForStorage()
            {
                byte[] ret = new byte[this.Value.Length + 1];
                ret[0] = (byte)(this.IsBinary ? 1 : 0);
                Array.Copy(this.Value, 0, ret, 1, this.Value.Length);
                return ret;
            }

            public static StoredField FromCassandraKeyValue(string storedFieldKey, byte[] value)
            {
                int delimeterPosition = storedFieldKey.IndexOf(Settings.Delimeter, StringComparison.Ordinal);
                string fieldName = storedFieldKey.Substring(0, delimeterPosition);
                int valueIndex = int.Parse(storedFieldKey.Substring(delimeterPosition + Settings.Delimeter.Length));

                byte[] realValue = new byte[value.Length - 1];
                Array.Copy(value, 1, realValue, 0, realValue.Length);

                return new StoredField(fieldName, valueIndex, (value[0] == 1), realValue);
            }
        }

        public class IndexedTerm
        {
            #region Fields

            List<int> positions;
            List<int> offsets;

            #endregion

            #region Properties [public]

            public string FieldName { get; private set; }
            public string TermText { get; private set; }
            public int Frequency { get; private set; }
            public IEnumerable<int> Positions
            {
                get { return this.positions; }
            }
            public int PositionCount
            {
                get { return this.positions.Count; }
            }
            public IEnumerable<int> Offsets
            {
                get { return this.offsets; }
            }
            public int OffsetCount
            {
                get { return this.offsets.Count; }
            }
            public bool StorePositions { get; private set; }
            public bool StoreOffsets { get; private set; }

            #endregion

            #region Constructors

            public IndexedTerm(string fieldName, string termText, bool storePositions, bool storeOffsets)
                : this(fieldName, termText, 0, storePositions, storeOffsets, null, null)
            {
            }

            public IndexedTerm(string fieldName, string termText, int frequency, bool storePositions, bool storeOffsets, IEnumerable<int> positions, IEnumerable<int> offsets)
            {
                this.FieldName = fieldName;
                this.TermText = termText;
                this.Frequency = frequency;
                this.StorePositions = storePositions;
                this.StoreOffsets = storeOffsets;

                this.positions = new List<int>(positions ?? new int[0]);
                this.offsets = new List<int>(offsets ?? new int[0]);
            }

            #endregion

            #region Methods [public]

            /// <summary>
            /// Gets the key for this IndexedTerm as it should be stored in Cassandra.
            /// </summary>
            /// <returns>The key for storage in Cassandra.</returns>
            public string GetIndexedTermKey()
            {
                return this.FieldName + Settings.Delimeter + this.TermText;
            }

            /// <summary>
            /// Add a term position to this IndexedTerm.
            /// </summary>
            /// <param name="position">The position of the term.</param>
            public void AddPosition(int position)
            {
                this.positions.Add(position);
            }

            /// <summary>
            /// Increment the term frequency.
            /// </summary>
            public void IncrementFrequency()
            {
                this.Frequency++;
            }

            /// <summary>
            /// Add an offset to this IndexedTerm.
            /// </summary>
            /// <param name="offset"></param>
            public void AddOffset(int offset)
            {
                this.offsets.Add(offset);
            }

            /// <summary>
            /// Gets the value of this IndexedTerm for storage in Cassandra.  Can be re-parsed using the static FromCassandraKeyValue() method.
            /// </summary>
            /// <returns>The byte-value of this IndexedTerm.</returns>
            public byte[] GetValueForStorage()
            {
                var byteBuffer = new ByteBuffer(32);

                // Create a flags byte to store information regarding storing positions & storing offsets.
                byte flags = 0;
                if (this.StorePositions)
                    flags |= 1;
                if (this.StoreOffsets)
                    flags |= 2;

                // Write the flags byte to the buffer.
                byteBuffer.WriteByte(flags);

                // Write the term frequency to the buffer.
                byteBuffer.WriteVariableInt32(this.Frequency);

                // If positions are stored, iterate through each position and write it to the buffer.
                if (this.StorePositions)
                {
                    for (int i = 0; i < this.positions.Count; i++)
                        byteBuffer.WriteVariableInt32(this.positions[i]);
                }

                // If offsets are stored, write the offset count to the buffer and then iterate through all offsets, writing them to the buffer.
                if (this.StoreOffsets)
                {
                    byteBuffer.WriteVariableInt32(this.offsets.Count);
                    for (int i = 0; i < this.offsets.Count; i++)
                        byteBuffer.WriteVariableInt32(this.offsets[i]);
                }

                return byteBuffer.GetBytes();
            }

            #endregion
            #region Methods [public] [static]

            /// <summary>
            /// Parses an IndexedTerm from the indexed term key (field name/term text) and the byte-value.  The byte value is stored in the format
            /// [flags] + [frequency] + [positions[]] + [offsets[]].
            /// </summary>
            /// <param name="indexedTermKey">The indexed term key (the key in Cassandra).  The format is (field name/term text).</param>
            /// <param name="value">The value stored in Cassandra which holds information about term frequency, positions and offsets.</param>
            /// <returns>A new IndexedTerm instance instantiated with values according to the provided term key and value as retrieved from Cassandra.</returns>
            public static IndexedTerm FromCassandraKeyValue(string indexedTermKey, byte[] value)
            {
                int delimeterPosition = indexedTermKey.IndexOf(Settings.Delimeter, StringComparison.Ordinal);

                string fieldName = indexedTermKey.Substring(0, delimeterPosition);
                string termText = indexedTermKey.Substring(delimeterPosition + Settings.Delimeter.Length);

                var byteBuffer = new ByteBuffer(value);

                var flags = byteBuffer.ReadByte();
                bool storePositions = (flags & 1) != 0;
                bool storeOffsets = (flags & 2) != 0;

                int frequency = byteBuffer.ReadVariableInt32();

                var positions = new List<int>();
                var offsets = new List<int>();

                if (storePositions)
                {
                    for (int i = 0; i < frequency; i++)
                        positions.Add(byteBuffer.ReadVariableInt32());
                }

                if (storeOffsets)
                {
                    int offsetCount = byteBuffer.ReadVariableInt32();
                    for (int i = 0; i < offsetCount; i++)
                        offsets.Add(byteBuffer.ReadVariableInt32());
                }

                return new IndexedTerm(fieldName, termText, frequency, storePositions, storeOffsets, positions, offsets);
            }

            #endregion
        }

        #endregion
    }
}
